Linux Hook 技术

Hook 是一种覆盖重写进程中符号的技术,在 Linux 中,通过环境变量 LD_PRELOAD 预加载包含同名符号的动态库即可实现。

覆盖 malloc 和 free 检查内存泄漏

1// 文件名: memcheck.c
2// 编译命令: gcc -o memcheck.so memcheck.c --shared -fPIC
3#define _GNU_SOURCE
4#include <dlfcn.h>
5#include <stdio.h>
6#include <stdlib.h>
7#include <stdbool.h>
8#include <execinfo.h>
9
10// 打印调用栈的最大深度
11#define MAX_STACK_DEPTH 16
12
13typedef struct RecordNode RecordNode;
14
15struct RecordNode
16{
17    RecordNode* next;
18    void* ptr;
19    size_t size;
20    void* stack_trace[MAX_STACK_DEPTH];
21    size_t stack_depth;
22};
23
24static RecordNode* head = NULL;                 // 此处头指针不存数据,head->next 才是第一个结点
25static RecordNode* tail = NULL;
26static void* (*real_malloc)(size_t) = NULL;     // 原 malloc 函数的地址
27static void (*real_free)(void*) = NULL;         // 原 free 函数的地址
28static bool ignore = false;                     // 忽略内部调用的 malloc
29
30// 打印调用栈
31static void printStack(const RecordNode* node)
32{
33    char** symbols = backtrace_symbols(node->stack_trace, node->stack_depth);
34    for (size_t i = 0; i < node->stack_depth; ++i) {
35        fprintf(stderr, " [%zu] %s \r\n", i, symbols[i]);
36    }
37    real_free(symbols);
38}
39
40// 打印内存泄漏记录
41static void printRecord(void)
42{
43    ignore = true;
44    for (RecordNode* node = head->next; node != NULL; node = node->next)
45    {
46        fprintf(stderr, "Leak %zu bytes at %p\n", node->size, node->ptr);
47        printStack(node);
48    }
49    ignore = false;
50}
51
52// 初始化
53static void init()
54{
55    // 通过 RTLD_NEXT 查找当前进程空间的下一个同名符号来获取原函数地址
56    real_malloc = (void*(*)(size_t))dlsym(RTLD_NEXT, "malloc");
57    real_free = (void(*)(void*))dlsym(RTLD_NEXT, "free");
58
59    head = (RecordNode*)real_malloc(sizeof(RecordNode));
60    head->next = NULL;
61    tail = head;
62    atexit(printRecord);
63}
64
65// 添加记录
66static RecordNode* addRecord(void* ptr, size_t size)
67{
68    RecordNode* node = (RecordNode*)real_malloc(sizeof(RecordNode));
69    node->next = NULL;
70    node->ptr = ptr;
71    node->size = size;
72    node->stack_depth = 0;
73
74    tail->next = node;
75    tail = node;
76    return node;
77}
78
79// 删除记录
80static void delRecord(void* ptr)
81{
82    RecordNode* prev = head;
83    for (RecordNode* node = head->next; node != NULL; node = node->next)
84    {
85        if (node->ptr == ptr)
86        {
87            prev->next = node->next;
88            if (node == tail)
89                tail = prev;
90            real_free(node);
91            break;
92        }
93        prev = node;
94    }
95}
96
97// hook malloc
98void* malloc(size_t size)
99{
100    if (real_malloc == NULL)
101        init();
102
103    void* ptr = real_malloc(size);
104
105    if (!ignore) // 防止内部调用 malloc 导致死循环
106    {
107        ignore = true;
108        RecordNode* node = addRecord(ptr, size);
109        node->stack_depth = backtrace(node->stack_trace, MAX_STACK_DEPTH);
110        ignore = false;
111    }
112    return ptr;
113}
114
115// hook free
116void free(void* ptr)
117{
118    real_free(ptr);
119    delRecord(ptr);
120}

预加载 memcheck.so 来检查内存泄漏:

构建程序(即下述的 test)时在链接选项中添加 -rdynamic 选项导出符号表才能显示函数名,否则只能显示地址。

1$ LD_PRELOAD=./memcheck.so ./test 
2Leak 233 bytes at 0x559e563c8350
3 [0] ./memcheck.so(malloc+0x7b) [0x7f6546b064a7] 
4 [1] ./test(func1+0x12) [0x559e5579215b] 
5 [2] ./test(func3+0x12) [0x559e55792185] 
6 [3] ./test(main+0x12) [0x559e557921a4] 
7 [4] /lib/x86_64-linux-gnu/libc.so.6(+0x29d90) [0x7f6546829d90] 
8 [5] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80) [0x7f6546829e40] 
9 [6] ./test(_start+0x25) [0x559e55792085] 
10Leak 666 bytes at 0x559e563c9560
11 [0] ./memcheck.so(malloc+0x7b) [0x7f6546b064a7] 
12 [1] ./test(func2+0x12) [0x559e55792170] 
13 [2] ./test(func3+0x1c) [0x559e5579218f] 
14 [3] ./test(main+0x12) [0x559e557921a4] 
15 [4] /lib/x86_64-linux-gnu/libc.so.6(+0x29d90) [0x7f6546829d90] 
16 [5] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80) [0x7f6546829e40] 
17 [6] ./test(_start+0x25) [0x559e55792085]